倘若系統發生部分的故障時,是否有辦法維持正常運作? Moleculer 內建了一些容錯功能,可以在 Broker 選項中啟用或關閉。
Moleculer 內建了一個斷路器機制。它設置了多個門檻值,可以在時間視窗內檢查失敗的請求機率,當機率到達指定的門檻值,它就會使斷路器跳閘。如果啟用斷路器,所有的服務呼叫都將接受斷路器的保護。
關於斷路器相關說明,可參考 CircuitBreaker[2] 及斷路器模式[3] 的文章。
範例:在 Boker 選項中啟用
| 名稱 | 類型 | 預設值 | 說明 | 
|---|---|---|---|
enabled | 
<Boolean> | false | 
啟用斷路器 | 
threshold | 
<Number> | 0.5 | 
門檻值。0.5 表示 50% 的機率。 | 
minRequestCount | 
<Number> | 20 | 
最小請求數。在此請求數以下不會跳閘。 | 
windowTime | 
<Number> | 60 | 
時間視窗的秒數。 | 
halfOpenTime | 
<Number> | 10000 | 
由 open 切換到 half-open 狀態所需的毫秒數 | 
check | 
<Function> | err && err.code >= 500 | 
用來檢查錯誤請求的函數 | 
const broker = new ServiceBroker({
    circuitBreaker: {
        enabled: true,
        threshold: 0.5,
        minRequestCount: 20,
        windowTime: 60, // 秒
        halfOpenTime: 5 * 1000, // 毫秒
        check: err => err && err.code >= 500
    }
});
當斷路器的狀態改變時,
ServiceBroker將會發送$circuit-breaker事件,請參考事件章節。
範例:在 Actions 中覆蓋全域設定
users.service.js
module.export = {
	name: "users",
	actions: {
		create: {
			circuitBreaker: {
				// 所有的斷路器選項都可以被覆蓋
				threshold: 0.3,
				windowTime: 30
			},
			handler(ctx) { }
		}
	}
};
Moleculer 有一個使用指數的迴避重試機制,可以嘗試重新呼叫失敗的請求。
| 名稱 | 類型 | 預設值 | 說明 | 
|---|---|---|---|
enabled | 
<Boolean> | false | 
啟用重試機制 | 
retries | 
<Number> | 5 | 
重試次數 | 
delay | 
<Number> | 100 | 
首次延遲時間。(毫秒) | 
maxDelay | 
<Number> | 2000 | 
最大延遲時間。(毫秒) | 
factor | 
<Number> | 2 | 
延遲的迴避因子。設為 1 則不做指數上升,設為 2 以上會由此指數,根據重試次數進行指數上升。 | 
check | 
<Function> | err && !!err.retryable | 
用來檢查錯誤請求的函數 | 
const broker = new ServiceBroker({
    retryPolicy: {
        enabled: true,
        retries: 5,
        delay: 100,
        maxDelay: 2000,
        factor: 2,
        check: err => err && !!err.retryable
    }
});
範例:呼叫時覆蓋全域設定
broker.call("posts.find", {}, { retries: 3 });
範例:在 Actions 中覆蓋全域設定
users.service.js
module.export = {
	name: "users",
	actions: {
		find: {
			retryPolicy: {
				// 所有的重試選項都可以被覆蓋
				retries: 3,
				delay: 500
			},
			handler(ctx) { }
		},
		create: {
			retryPolicy: {
				// 關閉此 Action 的重試機制
				enabled: false
			},
			handler(ctx) { }
		}
	}
};
回朔到前面 Broker 章節我們談過可以在 Broker 設定逾時。它可以在 ServiceBroker 選項或 broker.call 中覆蓋設定。在設定了逾時後若發生逾時事件, Broker 可以拋出一個 RequestTimeoutError 錯誤。
範例:在 Broker 選項中啟用
const broker = new ServiceBroker({
    requestTimeout: 5 * 1000 // 毫秒
});
範例:在 broker.call 時覆蓋掉全域設定
broker.call("posts.find", {}, { timeout: 3000 });
Moleculer 使用分散式逾時[4] 。在巢狀呼叫的情況下,逾時的值會隨著經過的時間減少。如果逾時值小於等於零會拋出
RequestTimeoutError錯誤,而下一個巢狀函數在呼叫時,會因為前一個逾時錯誤而拋出RequestSkippedError錯誤。
隔離模式就像船艙的水密門,可以將故障的服務隔離以維持服務穩定。 Moleculer 框架實作了隔離功能,用於控制 Actions 的併發請求處理。
| 名稱 | 類型 | 預設值 | 說明 | 
|---|---|---|---|
enabled | 
<Boolean> | false | 
啟用隔離功能 | 
concurrency | 
<Number> | 3 | 
最大併發執行次數 | 
maxQueueSize | 
<Number> | 10 | 
最大隊列大小 | 
範例:
const broker = new ServiceBroker({
    bulkhead: {
        enabled: true,
        concurrency: 3,
        maxQueueSize: 10,
    }
});
concurrency值可以限制併發請求執行次數。如果maxQueueSize設定大於零,所有的運算資源又被占滿,那麼 Broker 就會將請求儲存在隊列中。若隊列大小超過maxQueueSize的限制時, Broker 將會對超出的請求拋出QueueIsFull錯誤。
範例:在 Actions 中覆蓋全域設定
users.service.js
module.export = {
    name: "users",
    actions: {
        find: {
            bulkhead: {
                // 關閉此 Action 的隔離機制
                enabled: false
            },
            handler(ctx) {}
        },
        create: {
            bulkhead: {
                // 增加這個 Action 的併發數
                concurrency: 10
            },
            handler(ctx) {}
        }
    }
};
範例:在事件中覆蓋全域設定
my.service.js
module.exports = {
    name: "my-service",
    events: {
        "user.created": {
            bulkhead: {
                enabled: true,
                concurrency: 1
            },
            async handler(ctx) {
                // ...
            }
        }
    }
}
當錯誤發生時,如果你不想將錯誤響應給使用者, Fallback 可以呼叫其它 Actions 或是返回一些通用內容。你可以在 broker.call 的選項中定義 fallbackResponse 函數來返回任何的內容,但它必須是一個 Promise 函數。 Broker 會將 Context 與錯誤作為參數傳遞給 Fallback 函數。
範例:在 broker.call 選項設置 fallback 響應
const result = await broker.call("users.recommendation", { userID: 5 }, {
    timeout: 500,
    fallbackResponse(ctx, err) {
        // 由快取返回一個通用響應
        return broker.cacher.get("users.fallbackRecommendation:" + ctx.params.userID);
    }
});
範例:在 Actions 定義 fallback 函數
注意,當錯誤發生在 Action 的處理階段才會使用此 fallback 函數,如果在呼叫遠端節點時發生錯誤(例如逾時),則不使用此 fallback 函數。這種情況下請改在
broker.call選項中設置 fallback 函數。
module.exports = {
    name: "recommends",
    actions: {
        add: {
            fallback: (ctx, err) => "Some cached result",
            handler(ctx) {
                // ...
            }
        }
    }
};
範例:將 fallback 函數放在 method ,並以字串名稱設定 fallback 函數
module.exports = {
    name: "recommends",
    actions: {
        add: {
            // 當錯誤發生時呼叫 `getCachedResult` 方法
            fallback: "getCachedResult",
            handler(ctx) {
                // ...
            }
        }
    },
    methods: {
        getCachedResult(ctx, err) {
            return "Some cached result";
        }
    }
};
[1] Fault tolerance, https://moleculer.services/docs/0.14/fault-tolerance.html
[2] CircuitBreaker, https://martinfowler.com/bliki/CircuitBreaker.html
[3] Circuit Breaker pattern, https://docs.microsoft.com/azure/architecture/patterns/circuit-breaker
[4] Resilience for distributed systems, https://www.getambassador.io/learn/service-mesh/resilience-for-distributed-systems